// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: donggu

// bazel build //cc/appcoin2/experimental/donggu:strat

#include <array>
#include <experimental/filesystem>
#include <stdexcept>
#include <vector>

#include <cxxopts.hpp>
#include <highfive/H5DataSet.hpp>
#include <highfive/H5DataSpace.hpp>
#include <highfive/H5File.hpp>

#include "appcoin2/experimental/donggu/strat/feature_model.h"
#include "coin/proto/coin_order_enums.pb.h"
#include "coin/proto/coin_order_gateway.pb.h"
#include "coin2/app/driver.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/config/args.h"
#include "coin2/base/log.h"
#include "coin2/base/time.h"

std::vector<double> xs;  // 2d
std::vector<std::array<double, Y::NUM_Y>> ys;

struct PendingRow {
  int row;  // means ys[row] should be populated
  int window_size;
};

class Strategy : public ::coin2::strategy::IStrategy {
 public:
  Strategy() : y_3s_(this), y_5s_(this), y_10s_(this), y_30s_(this) {}

  void OnPushed(int64_t current_ts, const PendingRow& elem) {}
  void OnPopped(int64_t /*current_ts*/, const PendingRow& elem) {
    auto* x = &xs[elem.row * multi_product_model_.feature_size_];
    auto& y = ys[elem.row];
    if (elem.window_size == 3) {
      y[Y::AskToBid3s] = multi_product_model_.target_model_->features_[Features::Bid0p] -
                         x[multi_product_model_.target_model_offset_ + Features::Ask0p];
      y[Y::BidToAsk3s] = multi_product_model_.target_model_->features_[Features::Ask0p] -
                         x[multi_product_model_.target_model_offset_ + Features::Bid0p];
    } else if (elem.window_size == 5) {
      y[Y::AskToBid5s] = multi_product_model_.target_model_->features_[Features::Bid0p] -
                         x[multi_product_model_.target_model_offset_ + Features::Ask0p];
      y[Y::BidToAsk5s] = multi_product_model_.target_model_->features_[Features::Ask0p] -
                         x[multi_product_model_.target_model_offset_ + Features::Bid0p];
    } else if (elem.window_size == 10) {
      y[Y::AskToBid10s] = multi_product_model_.target_model_->features_[Features::Bid0p] -
                          x[multi_product_model_.target_model_offset_ + Features::Ask0p];
      y[Y::BidToAsk10s] = multi_product_model_.target_model_->features_[Features::Ask0p] -
                          x[multi_product_model_.target_model_offset_ + Features::Bid0p];
    } else if (elem.window_size == 30) {
      y[Y::Ready] = true;
      y[Y::AskToBid30s] = multi_product_model_.target_model_->features_[Features::Bid0p] -
                          x[multi_product_model_.target_model_offset_ + Features::Ask0p];
      y[Y::BidToAsk30s] = multi_product_model_.target_model_->features_[Features::Ask0p] -
                          x[multi_product_model_.target_model_offset_ + Features::Bid0p];
    } else {
      throw std::invalid_argument("a bug in the feature dumper");
    }
  }

  void Init(::coin2::app::Driver* driver) override {
    y_3s_.ResizeWindow(3 * SEC);
    y_5s_.ResizeWindow(5 * SEC);
    y_10s_.ResizeWindow(10 * SEC);

    multi_product_model_.Init(driver);

    for (const auto& [name, oe] : driver->strategy()->order_executors()) {
      (void)name;
      oe_ = oe.get();
      break;
    }
  }

  void onTradeFeed(const FeedUpdate& upd) override {
    multi_product_model_.onTradeFeed(upd);
    onFeed(upd);
  }
  void onBookFeed(const FeedUpdate& upd) override {
    multi_product_model_.onBookFeed(upd);
    onFeed(upd);
  }

  void onFeed(const FeedUpdate& upd) {
    // feature dump mode
    if (multi_product_model_.IsReady(upd.timestamp()) &&
        (throttle_.sample(upd.timestamp()) ||
         trade_qty_sampler_.sample(
             upd.timestamp(),
             multi_product_model_.target_model_->features_[Features::TradeQty5s]))) {
      xs.resize(xs.size() + multi_product_model_.feature_size_);  // new pending row
      double* x_last_row = &xs[xs.size() - multi_product_model_.feature_size_];
      ys.emplace_back();
      // copy current x features
      memcpy(
          x_last_row,
          multi_product_model_.features_.get(),
          multi_product_model_.feature_size_ * sizeof(double));

      y_3s_.Update(upd.timestamp(), {static_cast<int>(ys.size()) - 1, 3});
      y_5s_.Update(upd.timestamp(), {static_cast<int>(ys.size()) - 1, 5});
      y_10s_.Update(upd.timestamp(), {static_cast<int>(ys.size()) - 1, 10});
      y_30s_.Update(upd.timestamp(), {static_cast<int>(ys.size()) - 1, 30});
    } else {
      y_3s_.UpdateTimeOnly(upd.timestamp());
      y_5s_.UpdateTimeOnly(upd.timestamp());
      y_10s_.UpdateTimeOnly(upd.timestamp());
      y_30s_.UpdateTimeOnly(upd.timestamp());
    }

    // model mode

    // const double threshold = 0.5;
    // if (IsReady(upd.timestamp())) {
    //   auto& product_holder = product_cache_.holder(*target_);

    //   double& self = target_model_->features_[Features::Midp];
    //   double& ref1 = target_model_->features_[Features::Midp5s];
    //   double& ref2 = target_model_->features_[Features::Midp15s];
    //   double& ref3 = target_model_->features_[Features::Midp30s];
    //   double& spread = target_model_->features_[Features::SpreadMax5s];
    //   double price = target_model_->features_[Features::Ask0p] * 1.0001;
    //   double qty = 20;
    //   if (self-ref1>spread && self - ref1 > ref1 - ref2 && ref1 - ref2 > ref2 - ref3) {
    //     oe_->SubmitOrder(product_holder, price, qty, OrderDirection::DIRECTION_BUY,
    //     OrderDuration::IOC_ORDER, OrderType::LIMIT_ORDER,
    //     target_model_->features_[Features::Ask0p], 10, false, "");
    //   }

    //   if (ref1-self>spread && ref1 - self > ref2 - ref1 && ref2 - ref1 > ref3 - ref2) {
    //     oe_->SubmitOrder(product_holder, price, qty, OrderDirection::DIRECTION_BUY,
    //     OrderDuration::IOC_ORDER, OrderType::LIMIT_ORDER,
    //     target_model_->features_[Features::Ask0p], 10, false, "");
    //   }
    // }
  }

 public:
  MultiProductModel multi_product_model_;

 private:
  ::coin2::strategy::order_executor::OrderExecutorSystem* oe_ = nullptr;

  // y values
  ThrottleSampler throttle_;
  TradeQtySampler trade_qty_sampler_;
  MovingWindowWithCallee<PendingRow, Strategy> y_3s_;
  MovingWindowWithCallee<PendingRow, Strategy> y_5s_;
  MovingWindowWithCallee<PendingRow, Strategy> y_10s_;
  MovingWindowWithCallee<PendingRow, Strategy> y_30s_;
};

int main(int argc, char* argv[]) {
  google::InitGoogleLogging(argv[0]);

  DriverConfig app;

  cxxopts::Options opt("Feature Dumper", "dumper");
  AddDriverOptions(&opt);
  AddDateOption(&opt);
  AddProductOptions(&opt);
  opt.add_options()("name", "string", cxxopts::value<std::string>());

  auto res = opt.parse(argc, argv);
  ParseDriverOptions(res, &app);
  {
    auto ts = ParseDateOption(res);
    auto& interval = *app.mutable_feed()->mutable_common()->mutable_archive()->mutable_interval();
    interval.set_start(std::to_string(ts));
    interval.set_duration("PT24H");
    for (auto& [sys, ex] : *app.mutable_order()->mutable_exchanges()) {
      (void)sys;
      ex.mutable_sim_config()->set_start_timestamp(ts);
    }
  }

  ParseProductOptions(res, app.mutable_feed());

  if (res.count("name") != 1) {
    throw std::invalid_argument("requires --name");
  }
  std::string name = res["name"].as<std::string>();
  std::string out = fmt::format("out/feature/{}.h5", name);

  Strategy s;  // printer unused if feed-writer
  Driver driver(app, &s);
  driver.Run();

  {
    using namespace HighFive;
    std::experimental::filesystem::create_directories("out/feature");
    printf("num xy rows: %lu\n", ys.size());
    File file(out, File::ReadWrite | File::Create | File::Truncate);
    {
      std::vector<size_t> dims = {ys.size(), s.multi_product_model_.feature_size_};
      DataSet dx = file.createDataSet<double>("/x", DataSpace(dims));
      dx.write_raw(xs.data());
    }
    {
      std::vector<size_t> dims = {ys.size(), Y::NUM_Y};
      DataSet dy = file.createDataSet<double>("/y", DataSpace(dims));
      dy.write(reinterpret_cast<double(*)[Y::NUM_Y]>(ys.data()));
    }
  }

  return 0;
}
